import { pathToRegexp, Key } from 'path-to-regexp';
import { Builder } from './Builder';
import { Method } from '../utils/Method';
import { UsePlugin } from '../utils/usePlugin';
import { WebMiddleware, WebPlugin } from '../plugins/Plugin';
import { PluginQuery, QueryValidation } from '../plugins/PluginQuery';
import { PluginBody, BodyValidation } from '../plugins/PluginBody';
import { PluginParam, ParamsValidation } from '../plugins/PluginParam';
import { Validator } from '../validators/Validator';
import { PluginAction } from '../plugins/PluginAction';

interface Document {
  description?: string;
  remark?: string;
  category?: string;
}

export class WebBuilder<Props = any, State = any> extends Builder<Props, State> {
  protected readonly uris: string[];
  protected readonly methods: Method[];
  protected readonly uriPatterns: [RegExp, Key[]][];

  protected readonly document: Document = {};
  protected queryData: Record<string, Validator> = {};
  protected bodyData: Record<string, Validator> = {};
  protected paramData: Record<string, Validator> = {};

  constructor(prefix: string, uri: string | string[], methods: Method[]) {
    super();

    prefix = prefix.replace(/\/+$/, '');
    this.methods = methods;
    this.uris = Array.isArray(uri) ? uri : [uri];
    this.uriPatterns = this.uris.map((item) => {
      const realUri = prefix + (item === '/' ? '' : item);
      const keys: Key[] = [];
      const regexp = pathToRegexp(realUri, keys);

      return [regexp, keys];
    });
  }

  public use<P, S>(plugin: UsePlugin<P, S> | WebPlugin<P, S>): WebBuilder<Props & P, State & S> {
    this.plugins.use(plugin);
    return this;
  }

  public query<T extends { [key: string]: Validator }>(query: T): WebBuilder<Props & QueryValidation<T>, State> {
    this.queryData = query;
    this.plugins.use(new PluginQuery(query), 'private-web-query');
    return this;
  }

  public body<T extends { [key: string]: Validator }>(body: T): WebBuilder<Props & BodyValidation<T>, State> {
    this.bodyData = body;
    this.plugins.use(new PluginBody(body), 'private-web-body');
    return this;
  }

  public params<T extends { [key: string]: Validator }>(params: T): WebBuilder<Props & ParamsValidation<T>, State> {
    this.paramData = params;
    this.plugins.use(new PluginParam(params), 'private-web-params');
    return this;
  }

  public action<P = {}, S = {}>(fn: WebMiddleware<Props & P, State & S>): WebBuilder<Props & P, State & S> {
    this.plugins.use(new PluginAction(fn));
    return this;
  }

  public docs(document: Document): this {
    Object.assign(this.document, document);
    return this;
  }

  public/*protected*/ matchAndGetParams(path: string, method: Method): false | Record<string, string> {
    if (!this.methods.includes(method)) {
      return false;
    }

    for (const [regexp, keys] of this.uriPatterns) {
      let captures = path.match(regexp);

      if (captures === null) {
        continue;
      }

      const params: Record<string, any> = {};

      if (!keys.length) {
        return params;
      }

      for (let keyIndex = 0, length = captures.length; keyIndex < length; ++keyIndex) {
        if (keys[keyIndex]) {
          const capture = captures[keyIndex + 1];
          params[keys[keyIndex].name] = capture ? this.decodeURIComponent(capture) : capture;
        }
      }

      return params;
    }

    return false;
  }

  protected decodeURIComponent(text: string): any {
    try {
      return decodeURIComponent(text);
    } catch {
      return text;
    }
  }

  public/*protected*/ toJSON() {
    const transform = ([key, validator]: [string, Validator]) => {
      const options = validator.toJSON();
      return {
        ...options,
        name: options.name || key,
      };
    };

    return {
      uris: this.uris,
      methods: this.methods,
      category: this.document.category || '',
      description: this.document.description || '',
      remark: this.document.remark || '',
      queries: Object.entries(this.queryData).map(transform),
      bodies: Object.entries(this.bodyData).map(transform),
      params: Object.entries(this.paramData).map(transform),
    };
  }
}
